﻿// @file  ModBusMaster.cs
// @author doublecat
// @date   2023-04-25
// @brief
// 
// Copyright (C) 2020  doublecat doublecat@163.com
// 
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see http://www.gnu.org/licenses/.

#region 导入模块

using System.Collections;
using System.IO.Ports;
using log4net;
using log4net.Config;

#endregion

namespace ModBus {
    /// <summary>
    /// ModBus主机
    /// </summary>
    public sealed class ModBusMaster {
        private static readonly ILog Log = LogManager.GetLogger(typeof(ModBusMaster));

        /// <summary>
        /// 串口
        /// </summary>
        private readonly SerialPort _port;

        private bool _recvBusy;
        private ADU? _response;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="port">串口</param>
        public ModBusMaster(SerialPort port) {
            _port = port;

            _port.DataReceived  += _PortDataReceived;
            _port.PinChanged    += _port_PinChanged;
            _port.ErrorReceived += _port_ErrorReceived;

            XmlConfigurator.ConfigureAndWatch(new FileInfo("Config/log4net.config"));
            Log.Info("--------------------------------------------------------------------------------");
        }

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="portName">端口名</param>
        /// <param name="baud">通信速率</param>
        /// <param name="parity">奇偶校验位</param>
        /// <param name="dataBits">数据位</param>
        /// <param name="stopBits">停止位</param>
        public ModBusMaster(string   portName,
                            int      baud     = 9600,
                            Parity   parity   = Parity.None,
                            int      dataBits = 8,
                            StopBits stopBits = StopBits.One)
            : this(new SerialPort {
                                      BaudRate = baud,
                                      PortName = portName,
                                      Parity   = parity,
                                      DataBits = dataBits,
                                      StopBits = stopBits
                                  }) {
        }

        private void _port_ErrorReceived(object sender, SerialErrorReceivedEventArgs e) {
            throw new NotImplementedException();
        }

        private void _port_PinChanged(object sender, SerialPinChangedEventArgs e) {
            throw new NotImplementedException();
        }

        /// <summary>
        /// 接收数据处理
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="serialDataReceivedEventArgs"></param>
        /// <exception cref="ModBusCRCException"></exception>
        private void _PortDataReceived(object sender, SerialDataReceivedEventArgs serialDataReceivedEventArgs) {
            int sid = _port.ReadByte();
            Log.Debug($"recevied slave id:0x{sid:X2}");
            int fid = _port.ReadByte();
            Log.Debug($"recevied function id:0x{fid:X2}");

            if ((fid & 0x80) == 0x80) {
                int ec = _port.ReadByte();
                Log.Warn($"response exception:fid=0x{fid:X2},err=0x{ec:X2}({Enum.GetName(typeof(EnumExceptionCode), ec)})");
                _response = new ADU((byte)sid, (byte)fid, new[] { (byte)ec }, true);
            } else {
                // 正常响应
                int    bc = 4; // 数据长度
                byte[] data;   // 数据数组

                // read命令和write命令分开处理
                if (fid < 5) {
                    // 字节数量
                    bc = _port.ReadByte();
                    Log.Debug($"modbus response data size:0x{bc:X2}");
                    // 读取数据字节
                    byte[] rd = new byte[bc];
                    _port.Read(rd, 0, bc); // data bytes
                    Log.Debug($"received data:{rd.ToHexString()}");
                    // 构造数据包数据
                    data    = new byte[bc + 1];
                    data[0] = (byte)bc;
                    Array.Copy(rd, 0, data, 1, bc);
                } else {
                    data = new byte[bc];
                    _port.Read(data, 0, bc);
                    Log.Debug($"received data:{data.ToHexString()}");
                }

                // 创建响应数据包
                _response = new ADU((byte)sid, (byte)fid, data, true);
            }

            // 检查响应


            // 检查数据包CRC
            byte[] crcBytes = new byte[2] {
                                              (byte)_port.ReadByte(),
                                              (byte)_port.ReadByte(),
                                          };
            ushort crc = crcBytes.ToUShort(0);
            if (!_response.CheckCRC(crc)) {
                Log.Error($"CRC error! [expected:0x{_response.CRC:X4},actual:0x{crc:X4}]");
                throw new ModBusCRCException(_response.CRC, crc);
            }

            Log.Debug("CRC ok!");

            _recvBusy = false;
        }

        /// <summary>
        /// 当master收到从机的响应时发布此事件；
        /// </summary>
        public event EventHandler<ReceivedEventArgs>? Received;

        /// <summary>
        /// 打开从机设备
        /// </summary>
        public void Open() {
            if (_port.IsOpen)
                return;
            _port.Open();
            Log.Debug($"open modbus slave {_port.PortName}.");
        }

        /// <summary>
        /// 关闭从机设备
        /// </summary>
        public void Close() {
            if (_port.IsOpen)
                _port.Close();
            Log.Debug($"close modbus slave {_port.PortName}.");
        }

        private ADU Request(ADU adu) {
            byte[] bytes = adu.ToBytes();
            Open();
            // 发送数据
            _port.Write(bytes, 0, bytes.Length);
            Log.Debug($"write modbus request:{bytes.ToHexString()}");

            _recvBusy = true;
            // 等待数据接收完成
            while (_recvBusy) {
            }

            Close();
            return _response ?? throw new InvalidOperationException();
        }

        /// <summary>
        /// 读取指定长度的位线圈数据
        /// </summary>
        /// <returns></returns>
        public BitArray ReadCoils(byte sid, ushort start, ushort n) {
            // 创建数据包
            ADU adu = new ADU(sid, 1, start, n);

            // 请求从机数据
            adu = Request(adu);

            // 发布接收事件
            ReceivedEventArgs rea = new ReceivedEventArgs(adu);
            OnReceived(rea);

            return adu.Data.ToBitArray(1);
        }

        /// <summary>
        /// 读取离散数据
        /// </summary>
        /// <param name="sid">从机id</param>
        /// <param name="start">数据地址</param>
        /// <param name="n">数量</param>
        /// <returns></returns>
        public BitArray ReadDiscreteInput(byte sid, ushort start, ushort n) {
            ADU adu = new ADU(sid, 0x02, start, n);
            adu = Request(adu);
            return adu.Data.ToBitArray(1);
        }

        /// <summary>
        /// 读取保持寄存器值
        /// </summary>
        /// <param name="sid">从机id</param>
        /// <param name="start">起始地址</param>
        /// <param name="n">数量</param>
        /// <returns></returns>
        public ushort[] ReadHoldingRegisters(byte sid, ushort start, ushort n) {
            ADU adu = new ADU(sid, 0x03, start, n);
            adu = Request(adu);
            return adu.Data.ToUShortArray(1);
        }

        /// <summary>
        /// 读输入寄存器值
        /// </summary>
        /// <param name="sid">从机id</param>
        /// <param name="start">地址</param>
        /// <param name="n">数量</param>
        /// <returns></returns>
        public ushort[] ReadInputRegisters(byte sid, ushort start, ushort n) {
            ADU adu = new ADU(sid, 0x04, start, n);
            adu = Request(adu);
            return adu.Data.ToUShortArray(1);
        }

        /// <summary>
        /// 设置一个独立的线圈
        /// </summary>
        /// <param name="sid"></param>
        /// <param name="start">地址</param>
        /// <param name="on">线圈状态</param>
        public void WriteSingleCoil(byte sid, byte start, bool on) {
            ADU adu = new ADU(sid, 5, start, on);
            adu = Request(adu);
            OnReceived(new ReceivedEventArgs(adu));
        }

        /// <summary>
        /// 设置单个寄存器的值
        /// </summary>
        /// <param name="sid"></param>
        /// <param name="start"></param>
        /// <param name="value"></param>
        public void WriteSingleRegister(byte sid, ushort start, ushort value) {
            ADU adu = new ADU(sid, 6, new[] { start, value });
            adu = Request(adu);
            OnReceived(new ReceivedEventArgs(adu));
        }

        /// <summary>
        /// 设置多个线圈
        /// </summary>
        /// <param name="sid">从机ID</param>
        /// <param name="start">线圈地址</param>
        /// <param name="value">值，格式：7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8，...</param>
        public void WriteMultipleCoils(byte sid, ushort start, BitArray value) {
            ADU adu = new ADU(sid, 15, start, value);
            adu = Request(adu);
            OnReceived(new ReceivedEventArgs(adu));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="sid"></param>
        /// <param name="start"></param>
        /// <param name="value"></param>
        public void WriteMultipleRegisters(byte sid, ushort start, ushort[] value) {
            ADU adu = new ADU(sid, 16, start, value);
            adu = Request(adu);
            OnReceived(new ReceivedEventArgs(adu));
        }

        private void OnReceived(ReceivedEventArgs e) {
            Received?.Invoke(this, e);
            Log.Info($"publish received event:{e}");
        }
    }
}